package com.ea.admin.websocket;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ea.admin.mapper.DefaultCompanyConfMapper;
import com.ea.admin.mapper.UserInfoMapper;
import com.ea.admin.pojo.dto.UserInfoDto;
import com.ea.admin.pojo.po.DefaultCompanyConf;
import com.ea.admin.service.FileService;
import com.ea.admin.service.ReportService;
import com.ea.common.SA.FaceRegService;
import com.ea.common.SA.SAFeign;
import com.ea.common.SA.pojo.entity.DetectPersonResult;
import com.ea.common.result.BizException;
import com.ea.common.result.ErrorCode;
import feign.form.FormData;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.indunet.fastproto.FastProto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import ws.schild.jave.process.ProcessWrapper;
import ws.schild.jave.process.ffmpeg.DefaultFFMPEGLocator;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.stream.Collectors;

import static com.ea.admin.websocket.WebsocketErrorCode.*;

@Component
@ServerEndpoint("/websocket/{sid}")
@Slf4j
public class WebSocketUploadServer {
    //用来记录当前在线连接数
    private static int onlineCount = 0;

    //线程安全的set，用来存放每个客户端对应的websocket对象
    private static final CopyOnWriteArraySet<WebSocketUploadServer> webSocketSet = new CopyOnWriteArraySet<>();
    //与某个客户端的连接会话，需要通过它来给客户端发送数据
    // session 线程安全
    private Session session;
    // private Socket socket;
    static SAFeign saFeign;
    static FileService fileService;
    static ReportService reportService;
    static DefaultCompanyConfMapper defaultCompanyConfMapper;
    /*    final static String sampleFaceRegResult = "{\"data\":{\"company\":\"样例数据\",\"department\":\"样例数据\",\"name\":\"样例数据\"},\"type\":1}";
        final static String sampleVirResult = "{\"data\":{\"properties\":[{\"propertiesName\":\"攻击性\",\"value\":0.75150263},{\"propertiesName\":\"焦虑\",\"value\":0.47187573},{\"propertiesName\":\"压力\",\"value\":0.123313196},{\"propertiesName\":\"可疑\",\"value\":0.44599187}]},\"type\":2}";
        final static String sampleFaceResult = "{\"data\":{\"bottom\":479,\"left\":92,\"right\":524,\"top\":83},\"type\":3}";
        final static String sampleReportResult = "{\"data\":\"1774811018885115906\",\"type\":4}";
    */
    static FaceRegService faceRegService;
    static UserInfoMapper userInfoMapper;

    private static void blockFfmpeg(BufferedReader br) throws IOException {
        String line;
        // 该方法阻塞线程，直至合成成功
        while ((line = br.readLine()) != null) {
            doNothing(line);
        }
    }

    private static void doNothing(String line) {
        System.out.println(line);
    }

    @Autowired
    public void setFileService(FileService fileService) {
        WebSocketUploadServer.fileService = fileService;
    }

    @Autowired
    public void setSaFeign(SAFeign saFeign) {
        WebSocketUploadServer.saFeign = saFeign;
    }

    @Autowired
    public void setFaceRegService(FaceRegService faceRegService) {
        WebSocketUploadServer.faceRegService = faceRegService;
    }

    @Autowired
    public void setReportService(ReportService reportService) {
        WebSocketUploadServer.reportService = reportService;
    }

    @Autowired
    public void setDefaultCompanyConfMapper(DefaultCompanyConfMapper defaultCompanyConfMapper) {
        WebSocketUploadServer.defaultCompanyConfMapper = defaultCompanyConfMapper;
    }

    @Autowired
    public void setUserInfoMapper(UserInfoMapper userInfoMapper) {
        WebSocketUploadServer.userInfoMapper = userInfoMapper;
    }

    /**
     * 原子性的++操作
     */
    public static synchronized void addOnlineCount() {
        WebSocketUploadServer.onlineCount++;
    }

    /**
     * 原子性的--操作
     */
    public static synchronized void subOnlineCount() {
        WebSocketUploadServer.onlineCount--;
    }

    //连接成功时，服务端会自动调用这个方法
    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) {
        log.info("sid: {} 尝试连接", sid);
        //设置缓冲区大小
        session.setMaxTextMessageBufferSize(51200000);
        session.setMaxBinaryMessageBufferSize(51200000);
        //这一句是注入session，否则this无法发送消息
        this.session = session;
        webSocketSet.add(this);
        addOnlineCount();

        log.info(sid + "------连接成功---当前在线人数为" + onlineCount);
    }

    /**
     * 连接关闭时调用的方法
     */
    @OnClose
    public void onClose(@PathParam("sid") String sid) {
        //在线人数减1
        subOnlineCount();
        //从set中删除
        webSocketSet.remove(this);
        log.info(sid + "已关闭连接" + "----剩余在线人数为：" + onlineCount);
    }

    @OnMessage
    public void onMessage(String message) {
        log.info("webSocket后台收到消息：" + message);
    }

    @OnMessage
    public void onMessage(byte[] message) {
        // 将接收到的二进制消息转换成Java Object
        WebsocketParam param = parseMessageToParam(message);
        switch (param.getType()) {
            case 1: {
                if (param.getFile() == null) System.out.println("传输的内容为空！");
                String filePath;
                try {
                    filePath = fileService.saveWebmFileFromBytes(param.getFile(), param.getFileUUID(), param.getIndex());
                } catch (BizException e) {
                    if (e.getErrorCode() == ErrorCode.SYSTEM_ERROR_FILE_UPLOAD_ERROR) {
                        UploadFileVo uploadFileVo = new UploadFileVo(param.getIndex(), param.getFileUUID());
                        handleError(FILE_UPLOAD_ERROR, "文件上传错误", e, uploadFileVo);
                    } else {
                        handleError(SYSTEM_ERROR, "系统错误", e, null);
                    }
                    return;
                }
                //parseWebmToH264(filePath);
                String type = "webm";
                File file = new File(filePath + "." + type);
                FormData video;
                try {
                    video = new FormData(
                            "application/octet-stream",
                            file.getName(),
                            Files.readAllBytes(file.toPath())
                    );

                } catch (IOException e) {
                    UploadFileVo uploadFileVo = new UploadFileVo(param.getIndex(), param.getFileUUID());
                    handleError(FILE_UPLOAD_ERROR, "文件转换失败", e, uploadFileVo);
                    return;
                }
                new Thread(() -> {
                    faceReg(video);
                    maxFaceDetectVideo(video);
                }).start();
                // 发送一个确认的消息
                uploadFileSuccess(param.getFileUUID(), param.getIndex());
                break;
            }
            case 2: {
                if (param.getFile() == null) System.out.println("传输的内容为空！");
                try {
                    fileService.saveWebmFileFromBytes(param.getFile(), param.getFileUUID(), param.getIndex());
                } catch (BizException e) {
                    if (e.getErrorCode() == ErrorCode.SYSTEM_ERROR_FILE_UPLOAD_ERROR) {
                        UploadFileVo uploadFileVo = new UploadFileVo(param.getIndex(), param.getFileUUID());
                        handleError(FILE_UPLOAD_ERROR, "文件上传错误", e, uploadFileVo);
                    } else {
                        UploadFileVo uploadFileVo = new UploadFileVo(param.getIndex(), param.getFileUUID());
                        handleError(SYSTEM_ERROR, "系统错误", e, uploadFileVo);
                    }
                }
                // 发送一个确认的消息
                uploadFileSuccess(param.getFileUUID(), param.getIndex());
                break;
            }
            case 3: {
                try {
                    // 合并视频
                    if (!mergeWithOutTrans(param.getFileUUID(), "webm", "mp4")) {
                        // 合并视频不成功，此时错误信息已经发送给前端。
                        return;
                    }
                    // file path没有后缀
                    String filePath = fileService.getPathByType("mp4") + param.getFileUUID() + File.separator
                            + param.getFileUUID();
                    String type = "mp4";
                    File file = new File(filePath + "." + type);
                    MultipartFile multipartFile = fileService.convertFileToMultipartFile(file);
                    FormData video;
                    try {
                        video = new FormData(
                                "application/octet-stream",
                                multipartFile.getOriginalFilename(),
                                multipartFile.getBytes()
                        );

                    } catch (IOException e) {
                        handleError(FILE_MERGE_ERROR, "文件转换失败", e, null);
                        return;
                    }
                    Long userId = faceReg(video);
                    if (userId != null) {
                        // 上传到mysql
                        fileService.updateMysqlFile(filePath, "mp4", param.getFileUUID());
                        // 分析视频
                        Long reportId = reportService.analyzeVideo(
                                null,
                                userId,
                                param.getFileUUID(),
                                // 实时检测 type = 1
                                1
                        );
                        sendMessage(4, reportId.toString());
                        uploadFileSuccess(param.getFileUUID(), -1);
                    }
                } catch (Exception e1) {
                    handleError(FILE_MERGE_ERROR, "合并视频失败", e1, null);
                }
                break;
            }
            case 4: {
                if (param.getFile() == null) System.out.println("传输的内容为空！");
                try {
                    FormData photo = new FormData(
                            "application/octet-stream",
                            UUID.randomUUID() + ".jpeg",
                            param.getFile()
                    );
                    new Thread(() -> faceRegPhoto(photo)).start();
                } catch (Exception e) {
                    handleError(FILE_UPLOAD_ERROR, "文件转换失败", e, null);
                    return;
                }
                break;
            }
            case 5: {
                // 根据uuid获得未上传的文件的index list
                List<Integer> list = fileService.getUploadedList(param.getFileUUID(), "webm");
                UploadFileListVo vo = new UploadFileListVo(param.getFileUUID(), list);
                sendMessage(7, vo);
            }
        }
    }

    private void uploadFileSuccess(String fileUUID, Integer index) {
        UploadFileVo uploadFileVo = new UploadFileVo(index, fileUUID);
        sendMessage(6, uploadFileVo);
    }

    private void maxFaceDetectVideo(FormData video) {
        int score = -1;
        try {
            faceRegService.sim = (double) defaultCompanyConfMapper.selectById(57).getUpperValueInt() / 100;
            score = faceRegService.maxFaceDetectVideo(video);
        } catch (Exception ignored) {
            log.info("环境监测结果为空或转换结果不正确，可能是识别不到人脸");
        }
        sendMessage(5, score);
    }

    private Boolean mergeWithOutTrans(String fileUUID, String sourceType, String destType) {
        // 直接拼接 文件流
        String folder = fileService.getPathByType(sourceType) + fileUUID + File.separator;
        String outputPath = folder + fileUUID + "." + sourceType;
        File output = new File(outputPath);
        if (!output.exists()) {
            try {
                output.createNewFile();
            } catch (IOException e) {
                handleError(FILE_MERGE_ERROR, "创建文件失败", e, null);
                return false;
            }
        }
        Path dir = Paths.get(folder);
        try {
            List<Path> collect = Files.list(dir)
                    .filter(path -> path.toString().substring(path.toString().lastIndexOf(".") + 1).equals(sourceType)
                            && !path.toString().equals(outputPath))
                    .sorted((o1, o2) -> {
                        String p1 = o1.getFileName().toString();
                        String p2 = o2.getFileName().toString();
                        int l1 = p1.lastIndexOf("-");
                        int r1 = p1.lastIndexOf(".");
                        int l2 = p2.lastIndexOf("-");
                        int r2 = p2.lastIndexOf(".");
                        return Integer.valueOf(p2.substring(l2, r2)).compareTo(Integer.valueOf(p1.substring(l1, r1)));
                    }).collect(Collectors.toList());
            for (Path path : collect) {
                byte[] bytes = Files.readAllBytes(path);
                try (FileOutputStream fos = new FileOutputStream(output, true)) {
                    fos.write(bytes);
                } catch (IOException e) {
                    handleError(FILE_MERGE_ERROR, "写入文件失败", e, null);
                    return false;
                }
            }
        } catch (IOException e) {
            handleError(FILE_MERGE_ERROR, "文件夹不存在", e, null);
            return false;
        }
        // 将output转码为mp4
        try {
            parseWebmToMp4(folder + fileUUID, sourceType, destType);
            // 删除 所有 webm 临时文件
            List<Path> webmFiles = Files.list(dir)
                    .filter(path -> path.toString().endsWith(sourceType))
                    .collect(Collectors.toList());
            for (Path path : webmFiles) {
                Files.delete(path);
            }
        } catch (IOException e) {
            handleError(FILE_MERGE_ERROR, "转码失败", e, null);
            return false;
        }
        return true;
    }

    private Long faceReg(FormData video) {
        // 识别人脸
        try {
            faceRegService.sim = (double) defaultCompanyConfMapper.selectById(57).getUpperValueInt() / 100;
            DetectPersonResult detectPersonResult = faceRegService.detectPersonVideo(video);
            if (detectPersonResult == null) {
                handleError(
                        FACE_REG_VIDEO_ERROR,
                        "人脸信息不存在",
                        new BizException(ErrorCode.TPS_FILE_UPLOAD_NO_FACE_ERROR),
                        null
                );
            } else {
                Long userId = detectPersonResult.getPersonId();
                UserInfoDto dto = userInfoMapper.selectUserDtoById(userId);
                sendMessage(1, parseUserInfoToFaceUserInfoVo(dto));
                return userId;
            }
        } catch (Exception e) {
            log.info("人脸检测失败，可能是人脸未注册，或识别不出人脸");
            handleError(
                    FACE_REG_VIDEO_ERROR,
                    "人脸检测失败，可能是人脸未注册，或识别不出人脸",
                    e,
                    null
            );
        }
        return null;
    }

    private Long faceRegPhoto(FormData video) {
        // 识别人脸
        try {
            faceRegService.sim = (double) defaultCompanyConfMapper.selectById(57).getUpperValueInt() / 100;
            DetectPersonResult detectPersonResult = faceRegService.detectPersonPhoto(video);
            if (detectPersonResult == null) {
                handleError(
                        FACE_REG_PHOTO_ERROR,
                        "人脸信息不存在",
                        new BizException(ErrorCode.TPS_FILE_UPLOAD_NO_FACE_ERROR),
                        null);
            } else {
                Long userId = detectPersonResult.getPersonId();
                UserInfoDto dto = userInfoMapper.selectUserDtoById(userId);
                sendMessage(1, parseUserInfoToFaceUserInfoVo(dto));
                return userId;
            }
        } catch (Exception e) {
            log.info("人脸检测失败，可能是人脸未注册，或识别不出人脸");
            handleError(
                    FACE_REG_PHOTO_ERROR,
                    "人脸检测失败，可能是人脸未注册，或识别不出人脸",
                    e,
                    null
            );
        }
        return null;
    }

    private FaceUserInfoVo parseUserInfoToFaceUserInfoVo(UserInfoDto userInfo) {
        FaceUserInfoVo vo = new FaceUserInfoVo();
        vo.setName(userInfo.getName());
        vo.setDepartment(userInfo.getDeptName());
        vo.setCompany(userInfo.getCompanyName());
        return vo;
    }

    private void readSocket(InputStream read) throws IOException {
        byte[] length = new byte[4];
        read.read(length);
        int len = ByteBuffer.wrap(length).getInt();
        byte[] response = new byte[len];
        read.read(response);
        // 将response转换成字符串打印
        String responseString = new String(response);
        sendMessage(parseByteToWebsocketVo(responseString));
    }

    private WebsocketVo parseByteToWebsocketVo(String responseString) {
        // 将 response 转换成string，然后使用fastjson解析
        JSONObject jsonObject = JSON.parseObject(responseString);
        SocketResult result = JSON.toJavaObject(jsonObject, SocketResult.class);
        if (result.getType() == 0) {
            AnalyResult analyResult = JSONObject.toJavaObject(jsonObject.getJSONObject("data"), AnalyResult.class);
            // 从数据库中查出对应的结果，类似 save vibra result
            // 查出系统当前的默认配置
            AnalyResultVo vo = new AnalyResultVo();
            vo.properties = new ArrayList<>();
            List<DefaultCompanyConf> defaultCompanyConfList = defaultCompanyConfMapper.selectList(null);
            defaultCompanyConfList.forEach(defaultCompanyConf -> {
                // key 去掉 name
                switch (defaultCompanyConf.getDefaultCompanyConfId().intValue()) {
                    case 7:
                        // 攻击性
                        vo.properties.add(new AnalyResultVoItem(
                                defaultCompanyConf.getLowerValueString(),
                                analyResult.VI_VAR_STAT_RES_P7));
                        break;
                    case 12:
                        // 压力
                        vo.properties.add(new AnalyResultVoItem(
                                defaultCompanyConf.getLowerValueString(),
                                analyResult.VI_VAR_STAT_RES_P6));
                        break;
                    case 8:
                        // 焦虑
                        vo.properties.add(new AnalyResultVoItem(
                                defaultCompanyConf.getLowerValueString(),
                                analyResult.VI_VAR_STAT_RES_F5X));
                        break;
                    case 14:
                        // 可疑
                        vo.properties.add(new AnalyResultVoItem(
                                defaultCompanyConf.getLowerValueString(),
                                analyResult.VI_VAR_STATE_VAR));
                }
            });
            WebsocketVo websocketVo = new WebsocketVo();
            websocketVo.setType(2);
            websocketVo.setData(vo);
            return websocketVo;
        }
        // 人脸结果
        else if (result.getType() == 1) {
            FaceInfoVo faceResult = JSONObject.toJavaObject(jsonObject.getJSONObject("data"), FaceInfoVo.class);
            WebsocketVo websocketVo = new WebsocketVo();
            websocketVo.setType(3);
            websocketVo.setData(faceResult);
            return websocketVo;
        }
        return null;
    }

    private void parseWebmToMp4(String filePath, String sourceType, String destType) throws IOException {
        if (Objects.equals(sourceType, "webm")) {
            ProcessWrapper ffmpeg = new DefaultFFMPEGLocator().createExecutor();
            ffmpeg.addArgument("-loglevel");
            ffmpeg.addArgument("error");
            ffmpeg.addArgument("-i");
            ffmpeg.addArgument(filePath + "." + sourceType);
            ffmpeg.addArgument("-c:v");
            ffmpeg.addArgument("copy");
            // ffmpeg.addArgument("libx264");
            // ffmpeg.addArgument("-strict");
            // ffmpeg.addArgument("experimental");
            // ffmpeg.addArgument("-y");
            ffmpeg.addArgument("-an");
            ffmpeg.addArgument(filePath + "." + destType);
            ffmpeg.execute();
            try (BufferedReader br = new BufferedReader(new InputStreamReader(ffmpeg.getErrorStream()))) {
                blockFfmpeg(br);
            }
        } else if (Objects.equals(sourceType, "h264")) {
            ProcessWrapper ffmpeg = new DefaultFFMPEGLocator().createExecutor();
            ffmpeg.addArgument("-loglevel");
            ffmpeg.addArgument("error");
            ffmpeg.addArgument("-i");
            ffmpeg.addArgument(filePath + "." + sourceType);
            ffmpeg.addArgument("-c:v");
            ffmpeg.addArgument("copy");
            ffmpeg.addArgument("-y");
            ffmpeg.addArgument(filePath + "." + destType);
            ffmpeg.execute();
            try (BufferedReader br = new BufferedReader(new InputStreamReader(ffmpeg.getErrorStream()))) {
                blockFfmpeg(br);
            }
        }
    }

    private WebsocketParam parseMessageToParam(byte[] message) {
        new WebsocketParam();
        WebsocketParam result = null;
        try {
            // 下面这行用来转换 string
            result = FastProto.parse(message, WebsocketParam.class);
            result.setType((ByteBuffer.wrap(message, 0, 2).getShort()));
            result.setIndex((ByteBuffer.wrap(message, 2, 4).getInt()));
            log.info("转换成功 result: {}", result);
            int offset = 42;
            byte[] file = new byte[message.length - offset];
            System.arraycopy(message, offset, file, 0, file.length);
            result.setFile(file);
        } catch (Exception e) {
            handleError(MESSAGE_RECEIVE_ERROR, "byte[]消息转换失败", e, null);
        }
        return result;
    }

    @SneakyThrows
    private void handleError(int code, String msg, Exception e, Object data) {
        WebsocketErrorCode errorCode = new WebsocketErrorCode(code, msg, data);
        WebsocketVo errorVo = new WebsocketVo(0, errorCode);
        try {
            sendMessage(errorVo);
        } catch (Exception e1) {
            throw new BizException(ErrorCode.SYSTEM_ERROR);
        }
        throw e;
    }

    //服务端向客户端发送消息
    public void sendMessage(String message) {
        try {
            if (this.session.isOpen()) {
                this.session.getBasicRemote().sendText(message);
            } else {
                log.warn("session is closed, data send fail.:{}", message);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new BizException(ErrorCode.SYSTEM_ERROR);
        }
    }

    private void sendMessage(WebsocketVo vo) {
        sendMessage(JSON.toJSONString(vo));
    }

    private void sendMessage(int type, Object data) {
        WebsocketVo vo = new WebsocketVo(type, data);
        sendMessage(vo);
    }

    @OnError
    public void onError(Session session, Throwable error) {
        // 处理WebSocket错误
        try {
            session.close();
        } catch (Exception e) {
            throw new BizException(ErrorCode.SYSTEM_ERROR);
        }
        error.printStackTrace();
    }
}
